"""
核心下载逻辑模块。
"""
import os
import shutil  # 导入 shutil 用于文件复制
import requests
from PIL import Image
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
import io
from typing import Tuple, Optional, Callable

from src.utils import tile_bounds_to_pixels

# 配置日志
# 注意：GUI 中会重新配置日志处理器，这里的配置可能不会生效，但保留以防万一
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


class TileDownloader:
    """
    瓦片下载器类，负责根据参数下载地图瓦片。
    """

    def __init__(self, url_template: str, output_dir: str, zoom_levels: Tuple[int, int],
                 tile_format: str, path_format: str, is_local_source: bool,
                 max_workers: int = 5,
                 clip_tiles: bool = False,
                 clip_bounds: Optional[Tuple[float, float, float, float]] = None
                 ):
        """
        初始化下载器。

        Args:
            url_template (str): 瓦片服务 URL 模板或本地路径模板，包含 {z}, {y}, {x}。
            output_dir (str): 本地保存瓦片的根目录。
            zoom_levels (Tuple[int, int]): 下载的缩放级别范围 (min_zoom, max_zoom)。
            tile_format (str): 保存的图片格式 ('png' 或 'jpg')。
            path_format (str): 本地路径存储格式 ('zxy', 'zyx')。
            is_local_source (bool): 指示瓦片源是否为本地路径。
            max_workers (int): 并发下载/复制线程数 (默认: 5)。
            clip_tiles (bool): 是否对超出范围的瓦片进行裁切 (默认: False)。
            clip_bounds (Optional[Tuple[float, float, float, float]]):
                裁切的经纬度范围 (lon_min, lat_min, lon_max, lat_max)。
                如果 clip_tiles 为 True，则此参数必须提供。
        """
        self.url_template = url_template
        self.output_dir = Path(output_dir)
        self.min_zoom, self.max_zoom = zoom_levels
        self.tile_format = tile_format.lower()
        self.path_format = path_format.lower()
        self.is_local_source = is_local_source
        self.max_workers = max_workers
        self.session = requests.Session()
        self.session.headers.update({'User-Agent': 'map_tile_downloader/1.0'})

        self.clip_tiles = clip_tiles
        self.clip_bounds = clip_bounds
        if self.clip_tiles and self.clip_bounds is None:
            raise ValueError("启用 clip_tiles 时，必须提供 clip_bounds。")

    def get_tile_path(self, z: int, x: int, y: int) -> Path:
        """
        根据路径格式生成本地文件路径。 (改为公共方法供 GUI 预览使用)

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。

        Returns:
            Path: 本地文件的完整路径对象。
        """
        if self.path_format == 'zyx':
            return self.output_dir / str(z) / str(y) / f"{x}.{self.tile_format}"
        else:  # 默认 'zxy'
            return self.output_dir / str(z) / str(x) / f"{y}.{self.tile_format}"

    def _get_source_tile_path(self, z: int, x: int, y: int) -> Path:
        """
        根据源模板和路径格式生成源文件路径。
        注意：这里假设源路径格式与目标路径格式相同或可以通过模板推断。
        如果源和目标格式不同，需要更复杂的逻辑来映射。

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。

        Returns:
            Path: 源文件的完整路径对象。
        """
        # 使用 url_template 作为本地路径模板
        # 注意：这里假设源路径的文件扩展名与模板中 {x} 后的占位符一致或需要处理
        # 例如，模板 F:\data\{z}\{y}\{x}.png
        template_path_str = self.url_template.format(z=z, y=y, x=x)
        # 确保扩展名正确
        source_path = Path(template_path_str)
        if source_path.suffix.lower()[1:] != self.tile_format:  # 比较不含点的后缀
            source_path = source_path.with_suffix(f".{self.tile_format}")
        return source_path.resolve()  # 返回绝对路径

    def _copy_tile(self, z: int, x: int, y: int) -> bool:
        """
        从本地源复制单个瓦片到目标位置。

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。

        Returns:
            bool: 复制成功返回 True，否则 False。
        """
        try:
            source_path = self._get_source_tile_path(z, x, y)
            dest_path = self.get_tile_path(z, x, y)

            logger.debug(f"Copying tile from {source_path} to {dest_path}")

            if not source_path.exists():
                logger.warning(f"Source tile not found: {source_path}")
                return False

            # 确保目标目录存在
            dest_path.parent.mkdir(parents=True, exist_ok=True)

            # 执行复制
            shutil.copy2(source_path, dest_path)  # copy2 会尝试保留元数据

            logger.debug(f"Copied tile {z}/{x}/{y} to {dest_path}")
            return True

        except Exception as e:
            logger.error(f"Error copying tile {z}/{x}/{y} from {source_path}: {e}")
            return False

    def _download_tile(self, z: int, x: int, y: int,
                       clip: Optional[bool] = None,
                       clip_bounds: Optional[Tuple[float, float, float, float]] = None,
                       cancelled_flag: Optional[Callable[[], bool]] = None) -> Optional[bool]:
        """
        下载或复制单个瓦片并保存（可选裁切）。

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。
            clip (Optional[bool]): 是否对此瓦片进行裁切。None 表示使用实例默认值。
            clip_bounds (Optional[Tuple[float, float, float, float]]):
                裁切的经纬度范围。None 表示使用实例默认值。
            cancelled_flag (Optional[Callable[[], bool]]):
                一个无参数回调函数，返回 True 表示下载已被取消。
                如果提供，将在关键点进行检查。

        Returns:
            Optional[bool]: True 表示成功, False 表示失败, None 表示被取消。
        """
        # 在任务开始时检查取消标志
        if cancelled_flag and cancelled_flag():
            logger.debug(f"Tile {z}/{x}/{y} download cancelled before start.")
            return None  # 约定：None 表示被取消
        # 解析裁切参数
        use_clip = self.clip_tiles if clip is None else clip
        use_clip_bounds = self.clip_bounds if clip_bounds is None else clip_bounds

        if self.is_local_source:
            if use_clip and use_clip_bounds is not None:
                return self._copy_tile_with_clip(z, x, y, use_clip_bounds)
            else:
                # 如果不需要裁切或没有边界，则使用原始复制方法
                return self._copy_tile(z, x, y)
        else:
            return self._download_tile_from_web(z, x, y, clip=use_clip, clip_bounds=use_clip_bounds)

    def _copy_tile_with_clip(self, z: int, x: int, y: int,
                             clip_bounds: Tuple[float, float, float, float]) -> bool:
        """
        从本地源复制单个瓦片到目标位置，并进行裁切。

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。
            clip_bounds (Tuple[float, float, float, float]):
                裁切的经纬度范围 (lon_min, lat_min, lon_max, lat_max)。

        Returns:
            bool: 复制并裁切成功返回 True，否则 False。
        """
        try:
            source_path = self._get_source_tile_path(z, x, y)
            dest_path = self.get_tile_path(z, x, y)

            logger.debug(f"Copying and clipping tile from {source_path} to {dest_path}")

            if not source_path.exists():
                logger.warning(f"Source tile not found: {source_path}")
                return False

            # 确保目标目录存在
            dest_path.parent.mkdir(parents=True, exist_ok=True)

            lon_min, lat_min, lon_max, lat_max = clip_bounds

            # 加载源瓦片
            with Image.open(source_path) as img:
                original_mode = img.mode

                # 计算裁切区域（像素）
                left, top, right, bottom = tile_bounds_to_pixels(x, y, z, lon_min, lat_min, lon_max, lat_max)
                if left == 0 and top == 0 and right == 256 and bottom == 256:
                    logger.debug(
                        f"Tile {z}/{x}/{y} marked for clipping but is fully within bounds. Saving as full tile.")
                    return self._save_full_tile(img, dest_path)
                # 检查裁切区域是否有效
                elif left < right and top < bottom:
                    self._save_cropped_tile(img, (left, top, right, bottom), dest_path)
                    logger.debug(
                        f"Cropped and copied tile {z}/{x}/{y} to {dest_path} (crop box: {left},{top},{right},{bottom})")
                    return True
                else:
                    # 裁切区域无效，瓦片完全在范围外，不保存
                    logger.debug(f"Source tile {z}/{x}/{y} is completely outside clip bounds, not copied.")
                    return False  # 或 True，取决于定义

        except Exception as e:
            logger.error(f"Error copying/clipping tile {z}/{x}/{y} from {source_path}: {e}")
            return False

    def _download_tile_from_web(self, z: int, x: int, y: int,
                                clip: bool = False,
                                clip_bounds: Optional[Tuple[float, float, float, float]] = None,
                                cancelled_flag: Optional[Callable[[], bool]] = None) -> Optional[bool]:
        """
        从网络下载单个瓦片并保存（可选裁切）。

        Args:
            z (int): 缩放级别。
            x (int): 瓦片 x 坐标。
            y (int): 瓦片 y 坐标。
            clip (bool): 是否对此瓦片进行裁切。
            clip_bounds (Optional[Tuple[float, float, float, float]]):
                裁切的经纬度范围 (lon_min, lat_min, lon_max, lat_max)。
            cancelled_flag (Optional[Callable[[], bool]]):
                一个无参数回调函数，返回 True 表示下载已被取消。
                如果提供，将在关键点进行检查。

        Returns:
            Optional[bool]: True 表示成功, False 表示失败, None 表示被取消。
        """
        if cancelled_flag and cancelled_flag():
            return None
        try:
            url = self.url_template.format(z=z, y=y, x=x)
            logger.debug(f"Downloading tile: {url}")

            response = self.session.get(url, timeout=10)

            if cancelled_flag and cancelled_flag():
                return None

            response.raise_for_status()

            tile_path = self.get_tile_path(z, x, y)
            tile_path.parent.mkdir(parents=True, exist_ok=True)

            if clip and clip_bounds is not None:
                lon_min, lat_min, lon_max, lat_max = clip_bounds

                # 将完整瓦片加载到内存
                image_bytes = io.BytesIO(response.content)
                with Image.open(image_bytes) as img:
                    original_mode = img.mode
                    # 计算裁切区域（像素）
                    left, top, right, bottom = tile_bounds_to_pixels(x, y, z, lon_min, lat_min, lon_max, lat_max)
                    if left == 0 and top == 0 and right == 256 and bottom == 256:
                        logger.debug(
                            f"Tile {z}/{x}/{y} marked for clipping but is fully within bounds. Saving as full tile.")
                        return self._save_full_tile(img, tile_path)
                    # 检查裁切区域是否有效
                    elif left < right and top < bottom:
                        self._save_cropped_tile(img, (left, top, right, bottom), tile_path)
                        logger.debug(
                            f"Cropped and saved tile {z}/{x}/{y} to {tile_path} (crop box: {left},{top},{right},{bottom})")
                        return True
                    else:
                        # 裁切区域无效，意味着瓦片完全在范围外，可以选择不保存或保存空/默认瓦片
                        # 这里选择不保存
                        logger.debug(f"Tile {z}/{x}/{y} is completely outside clip bounds, not saved.")
                        return False  # 或者 True，取决于是否认为“不保存”算成功
            else:
                # --- 无裁切逻辑 ---
                image_bytes = io.BytesIO(response.content)
                with Image.open(image_bytes) as img:
                    return self._save_full_tile(img, tile_path)
                logger.debug(f"Saved full tile {z}/{x}/{y} to {tile_path}")
                return True

        except requests.exceptions.RequestException as e:
            logger.error(f"Error downloading tile {z}/{x}/{y} from {url}: {e}")
        except Exception as e:
            logger.error(f"Error processing tile {z}/{x}/{y}: {e}")
        return False

    def _save_full_tile(self, img: Image.Image, tile_path: Path) -> bool:
        """保存完整的瓦片图像。"""
        try:
            if self.tile_format in ('jpg', 'jpeg'):
                if img.mode in ('RGBA', 'LA', 'P'):
                    img = img.convert('RGB')
                img.save(tile_path, 'JPEG', quality=95)
            else:
                final_tile_path = tile_path.with_suffix('.png')
                final_tile_path.parent.mkdir(parents=True, exist_ok=True)
                img.save(final_tile_path, 'PNG')
            return True
        except Exception as e:
            logger.error(f"Error saving full tile to {tile_path}: {e}")
            return False

    def _save_cropped_tile(self, img: Image.Image, cut_range, tile_path: Path) -> bool:
        """保存裁切后的瓦片图像。"""
        try:
            # 执行裁切
            cropped_img = img.crop(cut_range)
            # 处理图像模式和保存
            new_img = Image.new("RGBA", img.size, (0, 0, 0, 0))
            new_img.paste(cropped_img, cut_range[:2])
            if self.tile_format in ('jpg', 'jpeg'):
                # JPG 不支持透明度
                if new_img.mode in ('RGBA', 'LA', 'P'):
                    new_img = new_img.convert('RGB')
                new_img.save(tile_path, 'JPEG', quality=95)
            else:  # 默认保存为 PNG
                # 确保文件扩展名正确
                final_tile_path = tile_path.with_suffix('.png')
                final_tile_path.parent.mkdir(parents=True, exist_ok=True)
                new_img.save(final_tile_path, 'PNG')
            return True
        except Exception as e:
            logger.error(f"Error saving cropped tile to {tile_path}: {e}")
            return False

    def download_range(self, min_x: int, min_y: int, max_x: int, max_y: int, zoom: int,
                       clip_override: Optional[bool] = None, cancelled_flag: Optional[Callable[[], bool]] = None):
        """
        下载或复制指定范围内某一级别的所有瓦片。

        Args:
            min_x (int): 范围最小 x 坐标。
            min_y (int): 范围最小 y 坐标。
            max_x (int): 范围最大 x 坐标。
            max_y (int): 范围最大 y 坐标。
            zoom (int): 当前操作的缩放级别。
            clip_override (Optional[bool]):
                是否对此范围内的所有瓦片进行裁切。
                None (默认): 使用实例的 self.clip_tiles 设置。
                True/False: 强制覆盖实例设置。
            cancelled_flag (Optional[Callable[[], bool]]):
                一个无参数回调函数，返回 True 表示下载已被取消。
                如果提供，将在关键点进行检查。
        """
        if cancelled_flag and cancelled_flag():
            logger.info(f"层级 {zoom} 的下载在开始前被取消。")
            return
        # 确定是否应用裁切
        apply_clip = self.clip_tiles if clip_override is None else clip_override

        total_tiles = (max_x - min_x + 1) * (max_y - min_y + 1)
        operation = "复制" if self.is_local_source else "下载"
        clip_info = " (裁切)" if apply_clip else ""
        logger.info(
            f"开始{operation}层级 {zoom}{clip_info}，瓦片数据 {total_tiles} 张 (范围: x={min_x}-{max_x}, y={min_y}-{max_y})")

        if total_tiles <= 0:
            logger.info(f"层级 {zoom} 没有待{operation}的瓦片。")
            return

        tasks = []
        successful_ops = 0
        failed_ops = 0
        cancelled_ops = 0

        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            if cancelled_flag and cancelled_flag():
                logger.info(f"层级 {zoom} 的任务提交被取消。")
                return
            for x in range(min_x, max_x + 1):
                for y in range(min_y, max_y + 1):
                    # 传递裁切参数给 _download_tile
                    future = executor.submit(self._download_tile, zoom, x, y, clip=apply_clip,
                                             cancelled_flag=cancelled_flag)
                    tasks.append((future, zoom, x, y))

            for future, z, x, y in tasks:
                # 在等待每个任务前检查取消
                if cancelled_flag and cancelled_flag():
                    logger.info(f"层级 {zoom} 等待任务完成时被取消。")
                    # 注意：已经提交的任务无法被取消，只能等待它们自然完成或检查内部标志
                    # 这里可以选择 break，但更常见的是等待已提交的完成
                    # 为了简化，我们继续等待，但任务内部会检查标志
                    pass  # 继续等待，让任务自己处理取消
                try:
                    success = future.result()
                    if success is True:
                        successful_ops += 1
                    elif success is False:
                        failed_ops += 1
                    elif success is None:  # 约定：None 表示任务被取消
                        cancelled_ops += 1
                except Exception as e:
                    failed_ops += 1
                    logger.error(f"Unhandled exception for tile {z}/{x}/{y}: {e}")

        logger.info(
            f"层级 {zoom} {operation}完成。成功: {successful_ops}, 失败: {failed_ops}, 取消: {cancelled_ops}, 总计: {total_tiles}")

    def download_global(self, global_zoom_level: int):
        """
        下载或复制从级别 1 到 global_zoom_level 的全球瓦片。

        Args:
            global_zoom_level (int): 操作全球瓦片的最高级别。
        """
        from .utils import get_global_tile_range
        operation = "复制" if self.is_local_source else "下载"
        logger.info(f"开始{operation}全球瓦片，最高级别 {global_zoom_level}")
        for zoom in range(1, global_zoom_level + 1):
            min_x, min_y, max_x, max_y = get_global_tile_range(zoom)
            self.download_range(min_x, min_y, max_x, max_y, zoom)
        logger.info(f"全球瓦片{operation}完成。")
